/**
* @@@ START COPYRIGHT @@@

Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.

* @@@ END COPYRIGHT @@@
 */

package org.trafodion.rest.client;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpVersion;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A wrapper around HttpClient which provides some useful function and
 * semantics for interacting with the REST gateway.
 */
public class Client {
  public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];

  private static final Log LOG = LogFactory.getLog(Client.class);

  private HttpClient httpClient;
  private Cluster cluster;

  private Map<String, String> extraHeaders;

  /**
   * Default Constructor
   */
  public Client() {
    this(null);
  }

  /**
   * Constructor
   * @param cluster the cluster definition
   */
  public Client(Cluster cluster) {
    this.cluster = cluster;
    MultiThreadedHttpConnectionManager manager = 
      new MultiThreadedHttpConnectionManager();
    HttpConnectionManagerParams managerParams = manager.getParams();
    managerParams.setConnectionTimeout(2000); // 2 s
    managerParams.setDefaultMaxConnectionsPerHost(10);
    managerParams.setMaxTotalConnections(100);
    extraHeaders = new ConcurrentHashMap<String, String>();
    this.httpClient = new HttpClient(manager);
    HttpClientParams clientParams = httpClient.getParams();
    clientParams.setVersion(HttpVersion.HTTP_1_1);
  }

  /**
   * Shut down the client. Close any open persistent connections. 
   */
  public void shutdown() {
    MultiThreadedHttpConnectionManager manager = 
      (MultiThreadedHttpConnectionManager) httpClient.getHttpConnectionManager();
    manager.shutdown();
  }

  /**
   * @return the wrapped HttpClient
   */
  public HttpClient getHttpClient() {
    return httpClient;
  }

  /**
   * Add extra headers.  These extra headers will be applied to all http
   * methods before they are removed. If any header is not used any more,
   * client needs to remove it explicitly.
   */
  public void addExtraHeader(final String name, final String value) {
    extraHeaders.put(name, value);
  }

  /**
   * Get an extra header value.
   */
  public String getExtraHeader(final String name) {
    return extraHeaders.get(name);
  }

  /**
   * Get all extra headers (read-only).
   */
  public Map<String, String> getExtraHeaders() {
    return Collections.unmodifiableMap(extraHeaders);
  }

  /**
   * Remove an extra header.
   */
  public void removeExtraHeader(final String name) {
    extraHeaders.remove(name);
  }

  /**
   * Execute a transaction method given only the path. Will select at random
   * one of the members of the supplied cluster definition and iterate through
   * the list until a transaction can be successfully completed. The
   * definition of success here is a complete HTTP transaction, irrespective
   * of result code.  
   * @param cluster the cluster definition
   * @param method the transaction method
   * @param headers HTTP header values to send
   * @param path the properly urlencoded path
   * @return the HTTP response code
   * @throws IOException
   */
  public int executePathOnly(Cluster cluster, HttpMethod method,
      Header[] headers, String path) throws IOException {
    IOException lastException;
    if (cluster.nodes.size() < 1) {
      throw new IOException("Cluster is empty");
    }
    int start = (int)Math.round((cluster.nodes.size() - 1) * Math.random());
    int i = start;
    do {
      cluster.lastHost = cluster.nodes.get(i);
      try {
        StringBuilder sb = new StringBuilder();
        sb.append("http://");
        sb.append(cluster.lastHost);
        sb.append(path);
        URI uri = new URI(sb.toString(), true);
        return executeURI(method, headers, uri.toString());
      } catch (IOException e) {
        lastException = e;
      }
    } while (++i != start && i < cluster.nodes.size());
    throw lastException;
  }

  /**
   * Execute a transaction method given a complete URI.
   * @param method the transaction method
   * @param headers HTTP header values to send
   * @param uri a properly urlencoded URI
   * @return the HTTP response code
   * @throws IOException
   */
  public int executeURI(HttpMethod method, Header[] headers, String uri)
      throws IOException {
    method.setURI(new URI(uri, true));
    for (Map.Entry<String, String> e: extraHeaders.entrySet()) {
      method.addRequestHeader(e.getKey(), e.getValue());
    }
    if (headers != null) {
      for (Header header: headers) {
        method.addRequestHeader(header);
      }
    }
    long startTime = System.currentTimeMillis();
    int code = httpClient.executeMethod(method);
    long endTime = System.currentTimeMillis();
    if (LOG.isDebugEnabled()) {
      LOG.debug(method.getName() + " " + uri + " " + code + " " +
        method.getStatusText() + " in " + (endTime - startTime) + " ms");
    }
    return code;
  }

  /**
   * Execute a transaction method. Will call either <tt>executePathOnly</tt>
   * or <tt>executeURI</tt> depending on whether a path only is supplied in
   * 'path', or if a complete URI is passed instead, respectively.
   * @param cluster the cluster definition
   * @param method the HTTP method
   * @param headers HTTP header values to send
   * @param path the properly urlencoded path or URI
   * @return the HTTP response code
   * @throws IOException
   */
  public int execute(Cluster cluster, HttpMethod method, Header[] headers,
      String path) throws IOException {
    if (path.startsWith("/")) {
      return executePathOnly(cluster, method, headers, path);
    }
    return executeURI(method, headers, path);
  }

  /**
   * @return the cluster definition
   */
  public Cluster getCluster() {
    return cluster;
  }

  /**
   * @param cluster the cluster definition
   */
  public void setCluster(Cluster cluster) {
    this.cluster = cluster;
  }

  /**
   * Send a HEAD request 
   * @param path the path or URI
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response head(String path) throws IOException {
    return head(cluster, path, null);
  }

  /**
   * Send a HEAD request 
   * @param cluster the cluster definition
   * @param path the path or URI
   * @param headers the HTTP headers to include in the request
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response head(Cluster cluster, String path, Header[] headers) 
      throws IOException {
    HeadMethod method = new HeadMethod();
    try {
      int code = execute(cluster, method, null, path);
      headers = method.getResponseHeaders();
      return new Response(code, headers, null);
    } finally {
      method.releaseConnection();
    }
  }

  /**
   * Send a GET request 
   * @param path the path or URI
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response get(String path) throws IOException {
    return get(cluster, path);
  }

  /**
   * Send a GET request 
   * @param cluster the cluster definition
   * @param path the path or URI
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response get(Cluster cluster, String path) throws IOException {
    return get(cluster, path, EMPTY_HEADER_ARRAY);
  }

  /**
   * Send a GET request 
   * @param path the path or URI
   * @param accept Accept header value
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response get(String path, String accept) throws IOException {
    return get(cluster, path, accept);
  }

  /**
   * Send a GET request 
   * @param cluster the cluster definition
   * @param path the path or URI
   * @param accept Accept header value
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response get(Cluster cluster, String path, String accept)
      throws IOException {
    Header[] headers = new Header[1];
    headers[0] = new Header("Accept", accept);
    return get(cluster, path, headers);
  }

  /**
   * Send a GET request
   * @param path the path or URI
   * @param headers the HTTP headers to include in the request, 
   * <tt>Accept</tt> must be supplied
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response get(String path, Header[] headers) throws IOException {
    return get(cluster, path, headers);
  }

  /**
   * Send a GET request
   * @param c the cluster definition
   * @param path the path or URI
   * @param headers the HTTP headers to include in the request
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response get(Cluster c, String path, Header[] headers) 
      throws IOException {
    GetMethod method = new GetMethod();
    try {
      int code = execute(c, method, headers, path);
      headers = method.getResponseHeaders();
      byte[] body = method.getResponseBody();
      return new Response(code, headers, body);
    } finally {
      method.releaseConnection();
    }
  }

  /**
   * Send a PUT request
   * @param path the path or URI
   * @param contentType the content MIME type
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response put(String path, String contentType, byte[] content)
      throws IOException {
    return put(cluster, path, contentType, content);
  }

  /**
   * Send a PUT request
   * @param cluster the cluster definition
   * @param path the path or URI
   * @param contentType the content MIME type
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response put(Cluster cluster, String path, String contentType, 
      byte[] content) throws IOException {
    Header[] headers = new Header[1];
    headers[0] = new Header("Content-Type", contentType);
    return put(cluster, path, headers, content);
  }

  /**
   * Send a PUT request
   * @param path the path or URI
   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
   * supplied
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response put(String path, Header[] headers, byte[] content) 
      throws IOException {
    return put(cluster, path, headers, content);
  }

  /**
   * Send a PUT request
   * @param cluster the cluster definition
   * @param path the path or URI
   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
   * supplied
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response put(Cluster cluster, String path, Header[] headers, 
      byte[] content) throws IOException {
    PutMethod method = new PutMethod();
    try {
      method.setRequestEntity(new ByteArrayRequestEntity(content));
      int code = execute(cluster, method, headers, path);
      headers = method.getResponseHeaders();
      content = method.getResponseBody();
      return new Response(code, headers, content);
    } finally {
      method.releaseConnection();
    }
  }

  /**
   * Send a POST request
   * @param path the path or URI
   * @param contentType the content MIME type
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response post(String path, String contentType, byte[] content)
      throws IOException {
    return post(cluster, path, contentType, content);
  }

  /**
   * Send a POST request
   * @param cluster the cluster definition
   * @param path the path or URI
   * @param contentType the content MIME type
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response post(Cluster cluster, String path, String contentType, 
      byte[] content) throws IOException {
    Header[] headers = new Header[1];
    headers[0] = new Header("Content-Type", contentType);
    return post(cluster, path, headers, content);
  }

  /**
   * Send a POST request
   * @param path the path or URI
   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
   * supplied
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response post(String path, Header[] headers, byte[] content) 
      throws IOException {
    return post(cluster, path, headers, content);
  }

  /**
   * Send a POST request
   * @param cluster the cluster definition
   * @param path the path or URI
   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
   * supplied
   * @param content the content bytes
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response post(Cluster cluster, String path, Header[] headers, 
      byte[] content) throws IOException {
    PostMethod method = new PostMethod();
    try {
      method.setRequestEntity(new ByteArrayRequestEntity(content));
      int code = execute(cluster, method, headers, path);
      headers = method.getResponseHeaders();
      content = method.getResponseBody();
      return new Response(code, headers, content);
    } finally {
      method.releaseConnection();
    }
  }

  /**
   * Send a DELETE request
   * @param path the path or URI
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response delete(String path) throws IOException {
    return delete(cluster, path);
  }

  /**
   * Send a DELETE request
   * @param cluster the cluster definition
   * @param path the path or URI
   * @return a Response object with response detail
   * @throws IOException
   */
  public Response delete(Cluster cluster, String path) throws IOException {
    DeleteMethod method = new DeleteMethod();
    try {
      int code = execute(cluster, method, null, path);
      Header[] headers = method.getResponseHeaders();
      byte[] content = method.getResponseBody();
      return new Response(code, headers, content);
    } finally {
      method.releaseConnection();
    }
  }
}
